Skip to content

Q21: reject a shadowed vdsim core at import time - #13

Merged
Onlyti merged 2 commits into
mainfrom
feat/import-shadow-guard
Sep 22, 2026
Merged

Onlyti merged 2 commits into
mainfrom
feat/import-shadow-guard

Conversation

@Onlyti

@Onlyti Onlyti commented Sep 22, 2026

Copy link
Copy Markdown
Owner

What

import vdsim can silently resolve to somebody else's package. Another PyPI
distribution ships a top-level vdsim/ package; installed in the same
environment as our wheel, that directory wins over vdsim.cpython-*.so. pip
neither warns nor overwrites, so the failure surfaces much later as an
AttributeError on an unrelated line.

The import name is unchanged. This PR only makes the shadowing detectable at
the point of import.

Shared check — one place

python/vdsim_guard.py (new, shipped in the wheel):

API Role
diagnose(mod) None if mod is the core, else a one-line reason
check_core(mod) returns mod or raises CoreShadowedError
load_core(fallback_paths=()) imports vdsim, validates, retries explicit build dirs

Two structural properties, both must hold:

  1. vdsim.__file__ ends with one of importlib.machinery.EXTENSION_SUFFIXES
  2. the module exposes SimSession, VehicleParams, TireParams, make_sim_session

The check never looks at a distribution name — the offending package is named
only inside the remedy sentence, as an example.

Failure is an ImportError subclass, not a warning.

Wiring

vdsim_lab, vdsim_plant and vdsim_rl are the only shipped modules that
import the core; all three now call load_core(). vdsim_render,
vdsim_render3d, vdsim_preset, vdsim_trace and vdsim_campaign never
import vdsim, so shadowing cannot affect them and they are untouched.

CLI entry points are covered transitively: vdsim-quickstart → vdsim_lab,
vdsim-campaign → child processes that import vdsim_lab. vdsim-render /
vdsim-render3d operate on .vdtrace files only.

load_core also retries the explicit build directory when the ambient import is
shadowed, so a repo checkout keeps working in an environment that has the other
package installed — strictly better than the old try/except ImportError.

Error message

'import vdsim' loaded /tmp/tmpXXXX/vdsim/__init__.py -- it is Python source, not a compiled extension module.
Cause: another installed distribution provides a top-level 'vdsim' package, which shadows VDSim's vdsim.cpython-*.so on sys.path (pip does not warn about this).
Fix: uninstall the shadowing distribution (e.g. `pip uninstall vehicle-dynamics-sim`) or install VDSim into its own virtual environment.

Tests

Added to tests/scripts/test_experiment_api.py (existing experiment_api
target — no new ctest target):

  • test_shadowed_core_is_rejected — a fake vdsim/ package first on
    PYTHONPATH, load_core([]) must raise and the message must name the real
    load path plus Cause: / Fix:
  • test_shadowed_core_recovers_from_build_dir — same shadow, but with the build
    directory as a fallback: the core wins
  • test_real_core_passes_the_check — the shipped .so passes diagnose

Negative verification: forcing check_core to skip the check makes
experiment_api Failed; restoring it makes it Passed.

Packaging

python/vdsim_guard.py added to the SKBUILD install list.
tools/wheel_import_smoke.py reads that list as its single source, so the new
module is covered by the pre-flight C-1 import closure automatically
(modules to import (15): vdsim, vdsim_guard, ...).

Counts

  • registered tests: 505, unchanged (no new target)
  • registration surface: python/CMakeLists.txt +1 line → the 3× identical-count
    contract applies. docs/VALIDATION.md currency is dropped to PENDING with a
    reason line; no number is written until CI measures this tree three times.
  • ISO / benchmark matrix rows: diff 0

Onlyti and others added 2 commits September 22, 2026 15:23
Another PyPI distribution ships a top-level vdsim/ package.  Installed beside
our wheel it wins the import over vdsim.cpython-*.so with no warning from pip,
and the failure only surfaces later as an AttributeError on an unrelated line.

python/vdsim_guard.py holds the decision once: a module bound to the name vdsim
must be a compiled extension (importlib EXTENSION_SUFFIXES) and expose the core
symbols.  load_core() also retries an explicit build directory, so a repo
checkout keeps working in an environment that has the shadowing package.

vdsim_lab, vdsim_plant and vdsim_rl now go through load_core(); the other
shipped modules never import the core, so they are untouched.  The error names
the file that was actually loaded plus a one-line cause and remedy; the check
itself is structural and never tests for a distribution name.

docs/VALIDATION.md drops to PENDING: python/CMakeLists.txt is part of the
registration surface, so the canonical count is re-measured on this tree.
Measured tree 7b7947f; three identical CI attempts on the same head. Numbers are in the PR body and the currency block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Onlyti
Onlyti merged commit daa2a80 into main Sep 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants